--[[ lib.lua
	This is a pseudo library designed for MicroMaco.
	Functions, variables, etc. declared here will
	be accessable in all scripts made for MicroMacro.

	Note: You should not modify this script unless
	you are fixing something. Please report fixes
	to the author (admin@solarstrike.net) or post
	on the forums at www.solarstrike.net

	Any changes you make may be overwritten if
	you chose to update. Consider making a module
	instead.
--]]

math.randomseed(getTime().low); -- Otherwise first random result is non-random!

------------------------------------------------------
-- Import configurations and libraries, load modules
------------------------------------------------------

-- Prep information gathered from config.lua
if( not allowSystemCommands ) then
	os.execute = function (cmd)
		error("System commands have been disabled. Unable to call os.execute().", 2);
	end

	io.popen = function (prog, mode)
		error("System commands have been disabled. Unable to call io.popen().", 2);
	end
end

if( type(moduleAutoload) ~= "table" ) then
	moduleAutoload = {};
end


-- Set module path
package.path = ".\\?.lua;" .. getPath() .. "/lib/mods/?.lua" ;
package.cpath = ".\\?.dll;" .. getPath() .. "/plugins/?.dll;" .. getPath() .. "/lib/mods/?.dll";

-- Load modules/plugins
local plugintab = getDirectory(getPath() .. "/plugins");
if( plugintab ) then
	for i,v in pairs(plugintab) do
		local extpos = string.find(v, ".dll");
		if( extpos ) then
			local fname = string.sub(v, 0, extpos - 1);

			-- Make sure it's not already loaded for some reason
			if( not package.loaded[fname] ) then
				local success = package.loadlib(getPath() .. "/plugins/" .. v, "luaopen_" .. fname);
				if( type(success) == "function" ) then
					package.loaded[fname] = success();
				else
					package.loaded[fname] = success;
				end

				if( success ) then
					logMessage("Loaded plugin \'" .. fname .. "\'");
				else
					logMessage("Failed to load plugin \'" .. fname .. "\'");
				end
			end
		end
	end
end


-- Autoload modules/plugins
for i,v in pairs(moduleAutoload) do
	require(v);
end



------------------------------------------------------
-- Keyboard & Language setup
------------------------------------------------------
key = nil;
if( keyboard ~= nil and type(keyboard) == "string" ) then
	key = require("keyboard/" .. keyboard);
end

if( key == true or key == nil ) then
	setTextColor(cli.yellow);
	error("Error loading keyboard module.");
	key = { };
end


------------------------------------------------------
-- VARIABLE DECLARATION
------------------------------------------------------

-- Our start/stop keys.
-- Only useful when using startMacro()
local startKeyDefault = key.VK_F5;
local stopKeyDefault = key.VK_F6;

local startKey = startKeyDefault;
local stopKey = stopKeyDefault;


-- Timed function list.
-- These are automatically "timed" when registered
local timerList = {}


-- Priority Defines -- DEPRECATED! Use priority.high, priority.low, or priority.normal instead
PRIORITY_HIGH = 1;
PRIORITY_NORMAL = 0;
PRIORITY_LOW = -1;

-- "Threading" stuff
local __threads = {};

-- Protected environment vars
local __PErunning = false;
local __PEco;


------------------------------------------------------
-- FUNCTION DECLARATION
------------------------------------------------------

function setStartKey(val)
    startKey = val
end

function getStartKey()
    return startKey
end

function setStopKey(val)
    stopKey = val
end

function getStopKey()
    return stopKey
end


-- Yields if in a couroutine and not main thread
function safeYield()
	-- make sure we're not trying to yield in the main thread
	-- do nothing.
	local co, main = coroutine.running();
	if( co == nil or main ) then
		return;
	end

	local status, err = pcall(coroutine.yield);

	if( status ~= true ) then
		setTextColor(cli.yellow);
		error(err, 3);
	end
end


-- unpacks varargs and returns them as a table.
-- also returns 'n', the true size of the table.
function unpack2(...)
	local n = select('#', ...);
	local t = {};
	for i = 1,n do
		local v = select(i, ...);
		t[i] = v;
	end

	return t, n;
end


-- Formatted output
-- C printf-like function
function printf(format, ...)
	local t, n = unpack2(...);

	for i = 1,n do
		local v = t[i];
		if( type(v) == "nil" ) then
			local err = sprintf("bad argument #%d to 'printf' (got %s)", i, type(v));
			error(err, 2);
		end

		if( type(v) == "table" or type(v) == "boolean" or type(v) == "function"
		or type(v) == "thread" or type(v) == "userdata" ) then
			t[i] = tostring(t[i]);
		end

	end

	local status, err = pcall(string.format, format, unpack(t));

	if( status == false ) then
		error(err, 2);
	end

	io.write(err);
end


-- Formatted output
-- C sprintf-like function
--sprintf = string.format;
function sprintf(format, ...)
	local t, n = unpack2(...);

	for i = 1,n do
		local v = t[i];
		if( type(v) == "nil" ) then
			local err = sprintf("bad argument #%d to 'sprintf' (got %s)", i, type(v));
			error(err, 2);
		end

		if( type(v) == "table" or type(v) == "boolean" or type(v) == "function"
		or type(v) == "thread" or type(v) == "userdata" ) then
			t[i] = tostring(t[i]);
		end

	end

	local status, err = pcall(string.format, format, unpack(t));

	if( status == false ) then
		error(err, 2);
	end

	return err;
end



-- Include another file
-- Directly calling dofile() may lead to issues with
-- relative paths. It is recommended to use include().
local __includes = {}; -- Holder table for included file names.
function include(file, forceInclude)
	if( forceInclude == nil ) then forceInclude = false; end;
	if( file == nil or string.len(file) < 1 ) then
		error("Cannot include \'nil\'.", 2);
	end

	local startExecutionPath = getExecutionPath();
	local test = string.find(file, "%a:[/\\]");
	local isRelative = ( test == nil );

	local fullpath, status, err;

	if( isRelative ) then
		fullpath = fixSlashes(startExecutionPath .. "/" .. file);
	else
		fullpath = fixSlashes(file);
	end

	-- Fix any "directory/../"
	fullpath = string.gsub(fullpath, "([%a%d%s%._]+)/%.%./", "");

	if( not forceInclude ) then
		if( __includes[fullpath] ) then
			return; -- Already included; exit
		end;
	end;

	setExecutionPath(getFilePath(fullpath));
	status, retval = pcall(dofile, fullpath);

	if( status ) then
		__includes[fullpath] = true;
	end;

	setExecutionPath(startExecutionPath);

	if( not status ) then
		error(retval, 2);
	end

	return retval;
end

-- Converts '\' to universally acceptable '/' for paths.
function fixSlashes(path, posix)
	if( posix == nil ) then
		posix = true
	end

	if( posix ) then
		path = string.gsub(path, "\\+", "/");
		--path = string.gsub(path, "%./+", "/");
		path = string.gsub(path, "//+", "/");
		return path;
	else
		path = string.gsub(path, "/+", "\\");
		path = string.gsub(path, "\\+", "\\");
		return path;
	end
end

-- Time conversion for timers

-- Convert hours to timer value
function hoursToTimer(hours)
	return math.floor( hours * 3600000 );
end

-- Convert minutes to timer value
function minutesToTimer(minutes)
	return math.floor( minutes * 60000 );
end

-- Converts seconds to timer value
function secondsToTimer(seconds)
	return math.floor( seconds * 1000 );
end


-- Prepares an at-exit callback function that is
-- called when the script terminates
local __EXIT_CALLBACK = nil;
function atExit(func)
	if( type(func) ~= "function" and type(func) ~= "nil" ) then
		local err = "Error: Non-function type passed to atExit() where a function is expected.";
		setTextColor(cli.yellow);
		error(err, 2);
		return;
	end

	__EXIT_CALLBACK = func;
end

-- Prepare an at-pause callback function
-- called when the script is paused.
local function defaultPauseCallback()
	printf("Paused.\n");
end

local __PAUSE_CALLBACK = defaultPauseCallback;
function atPause(func)
	if( type(func) ~= "function" and type(func) ~= "nil" ) then
		local err = "Error: Non-function type passed to atPause() where a function is expected.";
		setTextColor(cli.yellow);
		error(err, 2);
		return;
	end

	__PAUSE_CALLBACK = func;

	if( func == nil ) then
		__PAUSE_CALLBACK = defaultPauseCallback;
	end
end

local __ERROR_CALLBACK = nil;
function atError(func)
	if( type(func) ~= "function" and type(func) ~= "nil" ) then
		local err = "Error: Non-function type passed to atError() where a function is expected.";
		setTextColor(cli.yellow);
		error(err, 2);
		return;
	end

	__ERROR_CALLBACK = func;
end

-- Prepare an at-resume callback function
-- called when the script is resumed.
local function defaultResumeCallback()
	printf("Started.\n");
end

local __RESUME_CALLBACK = defaultResumeCallback;
function atResume(func)
	if( type(func) ~= "function" and type(func) ~= "nil" ) then
		local err = "Error: Non-function type passed to atResume() where a function is expected.";
		setTextColor(cli.yellow);
		error(err, 2);
		return;
	end

	__RESUME_CALLBACK = func;

	if( func == nil ) then
		__RESUME_CALLBACK = defaultResumeCallback;
	end
end


-- Register a function to be called automatically.
-- Should be used in combination with startMacro
-- as opposed to manually.
function registerTimer(name, time, func, ...)
	if( type(func) ~= "function" ) then
		local err = "Error: Non-function type passed to registerTimer() where a function is expected.";
		setTextColor(cli.yellow);
		error(err, 2);
		return; 
	end

	if( type(time) ~= "number" ) then
		local err = "Error: Non-numerical type passed to registerTimer() where a time value is expected.";
		setTextColor(cli.yellow);
		error(err, 2);
		return;
	end

	local tmp = {};
	tmp.time = time;
	tmp.func = func;
	tmp.args = unpack2(...);

	newTimer(name);
	startTimer(name, time);
	timerList[name] = tmp;
end

-- Unregisters a function from being called automatically.
-- Only works if the function has been registered
function unregisterTimer(name)
	if( timerList[name] ) then
  		timerList[name] = nil;
  		removeTimer(name);
	end;
end


-- Toggles __PErunning to 0 in order to stop
-- the protected environment from continuing.
function stopPE()
	__PErunning = false;
	--safeYield();
	error("Script forcibly terminated.", 0);
end

-- Start/resume the script
local function __start()
	for i,v in pairs(timerList) do
		if( isTriggered(i) ) then
			-- restart timers
			startTimer(i, v.time);
		end
	end

	if( __RESUME_CALLBACK ~= nil ) then
		__RESUME_CALLBACK();
	end

	__PErunning = true;
end

-- Stop/pause the script
local function __stop()
	if( __PAUSE_CALLBACK ~= nil ) then
		__PAUSE_CALLBACK();
	end

	__PErunning = false;--[[
	if( coroutine.running() ) then
		-- Do not yield from main thread...
		coroutine.yield();
	end]]
end

-- Get debug information from any running script
--[[
local script_line = 0;
local script_name = "";
local function get_script_info(event, line)
	script_line = line;
	script_name = debug.getinfo(2).short_src;
end
debug.sethook(get_script_info, "l");
]]

local ks = keyboardState();
local lastKS = ks;
local function checkGlobalHotkeys()
	local script_status = 'ok';

	local function pressed(vk)
		if( ks[vk] and not lastKS[vk] ) then
			return true;
		else
			return false;
		end
	end

	local check = true;
	local ah = getAttachedHwnd();
	local fw = foregroundWindow();
	if fw ~= getHwnd() and ( ah == 0 or fw ~= ah ) then
		check = false;
	end

	if( check ) then
		if( not __PErunning and pressed(startKey)) then
			script_status = 'start';
		elseif( __PErunning and pressed(stopKey) ) then
			script_status = 'stop';
		elseif( ks[key.VK_CONTROL] and pressed(key.VK_L) ) then
			script_status = 'kill';
		end
	end

	return script_status;
end

local function global_hotkey_hook(event, line)
	local ah = getAttachedHwnd();
	local fw = foregroundWindow();
	if( fw == getHwnd() or  ah == fw ) then
		if( keyPressed(key.VK_CONTROL) and keyPressed(key.VK_L) ) then
			stopPE();
		end
		--[[local ks = keyboardState();
		if( ks[key.VK_CONTROL] and ks[key.VK_L] ) then
			stopPE();
		end;]]
	end
end

-- Starts a "thread" (coroutine) that will
-- run in the background
function createThread(name, func, ...)
	if( __threads[name] ) then
		return false;
	end;

	local wrapped = function (...) coroutine.yield(); func(); end;

	local co = coroutine.create(wrapped);

	local status, err = coroutine.resume(co, ...);
	if( status == false ) then
		return false, err;
	end

	--table.insert(__threads, co);
	__threads[name] = co;
	return true;
end

-- Destroys a coroutine, reguardless of it's status
function killThread(name)
	__threads[name] = nil;
end

function getThreadStatus(name)
	if( not __threads[name] ) then
		return "dead";
	end

	return coroutine.status(__threads[name]);
end

-- Protected environment for startMacro to run.
-- Runs the function as a coroutine.
-- It allows us to easily run certain tasks
-- regularly without having to program them
-- directly into the macro.
function __ProtectedEnvironment(foo, defaultstate)
	if( defaultstate == false ) then
		__PErunning = false;

		if( startKey ~= 0 ) then
			printf("The macro is currently not running. Press the start key (%s) to begin.\n", getKeyName(startKey));
		end

		if( stopKey ~= 0 ) then
			printf("You may use (%s) key to stop/pause the script.\n", getKeyName(stopKey));
		end
	else
		__PErunning = true;

		if( stopKey ~= 0 ) then
			printf("Press the (%s) key to stop/pause the script.\n", getKeyName(stopKey));
		end

		if( startKey ~= 0 ) then
			printf("You can resume with the (%s) key.\n", getKeyName(startKey));
		end
	end

	local lastStartKey, lastStopKey = false, false;

  	__PEco = coroutine.create(function ()
			math.randomseed(getTime().low);
			foo();
		end
	);

	-- Check global hotkeys every x lines.
	-- This is to ensure that a thread doesn't get out of control
	-- and become unrecoverable.
	debug.sethook(__PEco, global_hotkey_hook, "l", 20);

	local timerCo = coroutine.create(function()
			math.randomseed(getTime().low);
			
			while(true) do
				-- automatic timer functions
				if( __PErunning ) then
					for i,v in pairs(timerList) do
						if( isTriggered(i) ) then
							startTimer(i, v.time); -- restart timer.

							if( type(v.func) ~= "function" ) then
								-- throw an error
								local err = sprintf("Timer \'%s\' error: invalid function", i);
								setTextColor(cli.yellow);
								error(err, 2);
							else
								v.func(unpack(v.args));
							end
						end
					end
				end

				coroutine.yield();
			end
		end
	);


	while( true ) do
		lastKS = ks;
		ks = keyboardState();
		local script_status = checkGlobalHotkeys();
		--[[if( script_status ~= 'ok' ) then
			print("Status", script_status);
		end]]
		if( script_status == 'kill' ) then
			break;
		elseif( script_status == 'stop' ) then
			__stop();
		elseif( script_status == 'start' ) then
			__start();
		end

		if( coroutine.status(__PEco) ~= 'dead' ) then
			-- continue macro
      
			if( __PErunning ) then
				-- Run main thread
				local runstatus,message = coroutine.resume(__PEco);
				if( runstatus == false ) then
					local name = debug.getinfo(__PEco, 0).short_src;
					local line = debug.getinfo(__PEco, 0).currentline;
					if( __ERROR_CALLBACK ) then __ERROR_CALLBACK(name, line, message); end;

					local msg = debug.traceback(__PEco, "In main thread:", 1) ..
					"\n\n----------TRACEBACK END----------\n\n";
					logRaw(msg);

					setTextColor(cli.yellow);
					error(message, 2);
				end

				-- Run timer thread
				runstatus,message = coroutine.resume(timerCo);
				if( runstatus == false ) then
					local name = debug.getinfo(__PEco, 0).short_src;
					local line = debug.getinfo(__PEco, 0).currentline;
					if( __ERROR_CALLBACK ) then __ERROR_CALLBACK(name, line, message); end;
					local msg = debug.traceback(timerCo, "In timer thread:", 1) ..
					"\n\n----------TRACEBACK END----------\n\n";
					logRaw(msg);

					setTextColor(cli.yellow);
					error(message, 2);
				end


				-- Run user threads
				for i,v in pairs(__threads) do
					if( coroutine.status(v) == 'dead' ) then
						__threads[i] = nil;
					else
						runstatus,message = coroutine.resume(v);
						if( runstatus == false ) then
							local name = debug.getinfo(__PEco, 0).short_src;
							local line = debug.getinfo(__PEco, 0).currentline;
							if( __ERROR_CALLBACK ) then __ERROR_CALLBACK(name, line, message); end;
							local msg = debug.traceback(v, "In user thread:", 1) ..
							"\n\n----------TRACEBACK END----------\n\n";
							logRaw(msg);

							setTextColor(cli.yellow);
							error(message, 2);
						end
					end
				end
			else
				rest(10); -- minimize CPU usage
			end
		else
			-- macro is "dead"
			break;
		end
	end

	-- cleanup timers
	for i,v in pairs(timerList) do
		timerList[i] = nil;
	end

	print("Stopping execution.");

	if( __EXIT_CALLBACK ~= nil ) then
		__EXIT_CALLBACK();
	end

	-- restore defaults
	startKey = startKeyDefault;
	stopKey = stopKeyDefault;
	atResume(nil);
	atPause(nil);
	atExit(nil);
end


-- startMacro passes the call off to
-- __ProtectedEnvironment, and handles
-- any errors.
function startMacro(foo, defaultstate)
	if( defaultstate ~= true ) then defaultstate = false; end;

	if( type(foo) ~= "function" ) then
		local err = "Error: Non-function type passed to startMacro(). Value is of type " .. type(foo) .. ".";
		setTextColor(cli.yellow);
		error(err, 2);
		return;
	end

	local status, err = pcall(__ProtectedEnvironment, foo, defaultstate);
	if( status ) then
		-- no errors
	else
		-- errors occured...log them
		setTextColor(cli.yellow);
		error(err, 3);
	end
end


-- A pretty standard rest/sleep command.
-- It automatically will yeild, though.
function yrest(msec)
	if( msec == nil ) then error("yrest() cannot rest for \'nil\'.\n", 2); end;
	safeYield();

	local resttime = 10;
	local sections;
	local ext;


	if( msec < resttime ) then
		rest(msec);
		return;
	else
		local startTime = getTime();
		while( deltaTime(getTime(), startTime) < msec ) do
			rest(resttime);
			safeYield();
		end
	end

end

-- Checks if a color is within 'accuracy' of another color.
-- Each channel is checked individually.
-- 'accuracy' should be between 0 and 255.
function colorMatch(color1, color2, accuracy)
	local r1, g1, b1, r2, g2, b2;

	if( accuracy == nil ) then accuracy = 0; end;
	if( accuracy < 0 ) then accuracy = 0; end;
	if( accuracy > 255 ) then accuracy = 255; end;

	r1 = getR(color1); g1 = getG(color1); b1 = getB(color1);
	r2 = getR(color2); g2 = getG(color2); b2 = getB(color2);

	if( math.abs(r2 - r1) <= accuracy and
		math.abs(g2 - g1) <= accuracy and
		math.abs(b2 - b1) <= accuracy ) then

		return true; -- they match
	end;

	return false; -- they don't match
end


-- Checks and returns true if a file exists.
function fileExists(fullpath)
	local handle = io.open(fullpath, "r");
	local success = handle ~= nil;

	if( success ) then
		handle:close();
	end

	return success;
end


-- "explode" a string into a table, splitting at 'token'
-- i.e. "Hello World, how are you?" explodes using the space
-- character would return {"Hello", "World,", "how", "are", "you?"}
-- if 'token' is not given, space is assumed.
function explode(instr, token)
	local findpos;
	local holder = {};

	if( token == nil or type(token) ~= "string" ) then token = " "; end

	findpos = string.find(instr, token, 1, true);

	if( findpos == nil ) then return {instr}; end

	while( findpos ) do
		table.insert(holder, string.sub(instr, 1, findpos - 1));
		instr = string.sub(instr, findpos + string.len(token));
		findpos = string.find(instr, token, 1, true);
	end

	if( string.len(instr) ) then
		table.insert(holder, instr);
	end

	return holder;
end





-- Throw a warning and log it
function warning(msg, showline)
	if( type(msg) ~= "string" ) then
		if( type(msg) == "number" ) then
			msg = tostring(msg);
		else
			error("Non-string passed to function warning", 2);
		end;
	end;

	if( type(showline) ~= "boolean" ) then
		showline = true;
	end;

	if( showline ) then
		local name = debug.getinfo(2).short_src;
		local line = debug.getinfo(2).currentline;
		msg = name .. ":" .. line .. "\n" .. msg;
	end

	setTextColor(cli.yellow);
	print("[WARN]: " .. msg);
	setTextColor(cli.lightgray);
	logMessage("[WARN]: " .. msg);
end


-- Similar to os.execute(), but returns the output as a string
-- NOTE: Includes trailing newline!
function system(cmd)
	local file = io.popen(cmd);
	local holder = "";

	for line in file:lines() do
		holder = holder .. line .. "\n";
	end

	file:close();
	return holder;
end